我们经常能听到映射和反射,那么它们代表的含义是什么呢?
映射只是一种概念,通常是用代码方式来表示一个比较复杂的意义。例如访问网站时 404 错误的含义是“找不到当前页”,500错误是“服务器内部错误”。那么我们会把这些错误的原因和一些固定的数字对应起来,这就是一种映射。
体现在程序里,映射用的比较多的地方就是和数据库打交道。我们通常会期望不直接操作数据库而对数据库做出一些改变。那么我们会声明一个类去对应数据库的一张数据表。
例如有一张学生表,有学号和姓名两个字段。我们可以在代码里建一个学生类,下面有学号和姓名两个属性,通过一定的技术可以把这个类和那张学生表对应起来,可以实现在操作这个类的时候,却去改变那张表的数据的效果,我们可以称这是一种映射。
熟悉 C#、Java 的朋友应该不难理解反射的,反射是一种技术,很多高级语言都有这种技术。具体作用就是通过一个独立存在的对象,可以找到该对象的其他信息。
例如该对象是由哪个类实例化而成的,例如有一条狗和一个人,我们通过反射技术,可以得知狗是犬科,人是灵长目的。犬科和灵长目就是我们通过反射所得到的信息,这两个词语不是一个层面的东西。
用代码来说,映射就是 a -> b
,反射就是 a.id
、a.func()
等。
在使用 Objective-C
开发时很少强调其反射概念,因为 Objective-C
的 Runtime
要比其他语言中的反射强大的多。在 Objective-C
中可以很简单的实现字符串和类型的转换 NSClassFromString
,实现动态方法调用 performSelector: withObject:
,动态赋值 KVC
等等。
这些功能大家已经习以为常,但是在其他语言中要实现这些功能却要跨过较高的门槛,而且有些根本就是无法实现的。不过在 Swift
中并不提倡使用 Runtime
,而是像其他语言一样使用反射 Reflect
。
Swift
的反射机制是基于一个叫 Mirror 的 struct
来实现的。你为具体的 subject
创建一个 Mirror
,然后就可以通过它查询这个对象 subject
。
在我们创建 Mirror
之前,我们先创建一个可以让我们当做对象来使用的简单数据结构。
|
|
创建一个 Mirror
创建 Mirror
最简单的方式就是使用 reflecting
构造器:
|
|
然后在 aBookmark
struct
上使用它:
|
|
这段代码创建了 Bookmark
的 Mirror
。正如你所见,对象的类型是 Any
。这是 Swift
中最通用的类型。Swift
中的任何东西至少都是 Any
类型的。这样一来 mirror
就可以兼容 struct
, class
, enum
, Tuple
, Array
, Dictionary
, set
等。
Mirror
结构体还有另外三个构造器,然而这三个都是在你需要自定义 mirror
这种情况下使用的。我们会在接下来讨论自定义 mirror
时详细讲解这些额外的构造器。
Mirror 中都有什么?
Mirror struct
中包含几个 types
来帮助确定你想查询的信息。
第一个是 DisplayStyle
enum
,它会告诉你对象的类型:
|
|
这些都是反射 API 的辅助类型。正如之前我们知道的,反射只要求对象是 Any
类型,而且 Swift
标准库中还有很多类型为 Any
的东西没有被列举在上面的 DisplayStyle
enum
中。如果试图反射它们中间的某一个又会发生什么呢?比如 closure
。
|
|
在这种情况下,这里你会得到一个 mirror
,但是 DisplayStyle
为 nil
。
也有提供给 Mirror
的子节点使用的 typealias
:
|
|
所以每个 Child
都包含一个可选的 label
和 Any
类型的 value
。为什么 label
是 Optional
的?如果你仔细考虑下,其实这是非常有意义的,并不是所有支持反射的数据结构都包含有名字的子节点。 struct
会以属性的名字做为 label
,但是 Collection
只有下标,没有名字。Tuple
同样也可能没有给它们的条目指定名字。
接下来是 AncestorRepresentation
enum
:
|
|
这个 enum
用来定义被反射的对象的父类应该如何被反射。也就是说,这只应用于 class
类型的对象。默认情况下 Swift
会为每个父类生成额外的 mirror
。然而,如果你需要做更复杂的操作,你可以使用 AncestorRepresentation enum
来定义父类被反射的细节。
如何使用一个 Mirror
现在我们有了给 Bookmark
类型的对象 aBookmark
做反射的实例变量 aMirror
。可以用它来做什么呢?
下面列举了 Mirror
可用的属性 / 方法:
let children: Children
:对象的子节点。displayStyle: Mirror.DisplayStyle?
:对象的展示风格let subjectType: Any.Type
:对象的类型func superclassMirror() -> Mirror?
:对象父类的mirror
下面我们会分别对它们进行解析。
DisplayStyle
很简单,它会返回 DisplayStyle
enum
的其中一种情况。如果你想要对某种不支持的类型进行反射,你会得到一个空的 Optional
值(这个之前解释过)。
|
|
Children
这会返回一个包含了对象所有的子节点的 AnyForwardCollection<Child>
。这些子节点不单单限于 Array
或者 Dictionary
中的条目。诸如 struct
或者 class
中所有的属性也是由 AnyForwardCollection<Child>
这个属性返回的子节点。AnyForwardCollection
协议意味着这是一个支持遍历的 Collection
类型。
|
|
SubjectType
这是对象的类型:
|
|
然而,Swift
的文档中有下面一句话:
“当
self
是另外一个mirror
的superclassMirror()
时,这个类型和对象的动态类型可能会不一样。”
SuperclassMirror
这是我们对象父类的 mirror
。如果这个对象不是一个类,它会是一个空的 Optional
值。如果对象的类型是基于类的,你会得到一个新的 Mirror
:
|
|
Struct 转 Core Data
假设我们在一个叫 Books Bunny 的新兴高科技公司工作,我们以浏览器插件的方式提供了一个人工智能,它可以自动分析用户访问的所有网站,然后把相关页面自动保存到书签中。
现在 Swift
已经开源,所以我们的后台服务端肯定是用 Swift
编写。因为在我们的系统中同时有数以百万计的网站访问活动,我们想用 struct
来存储用户访问网站的分析数据。不过,如果我们 AI 认定某个页面的数据是需要保存到书签中的话,我们需要使用 CoreData
来把这个类型的对象保存到数据库中。
现在我们不想为每个新建的 struct
单独写自定义的 Core Data
序列化代码。而是想以一种更优雅的方式来开发,从而可以让将来的所有 struct
都可以利用这种方式来做序列化。
那么我们该怎么做呢?
一个协议
记住,我们有一个 struct
,它需要自动转换为 NSManagedObject
(Core Data)。
如果我们想要支持不同的 struct
甚至类型,我们可以用协议来实现,然后确保我们需要的类型符合这个协议。所以我们假想的协议应该有哪些功能呢?
- 第一,协议应该允许自定义我们想要创建的 Core Data 实体的名字
- 第二,协议需要提供一种方式来告诉它如何转换为
NSManagedObject
。
我们的 protocol
(协议) 看起来是下面这个样子的:
|
|
toCoreData
方法使用了 Swift 2.0 新的异常处理来抛出错误,如果转换失败,会有几种错误情况,这些情况都在下面的 ErrorTypeenum
进行了列举:
|
|
上面列举了三种转换时需要注意的错误情况。第一种情况是我们试图把它应用到非 struct
的对象上。第二种情况是我们想要创建的 entity
在 Core Data 模型中不存在。第三种情况是我们想要把一些不能存储在 Core Data 中的东西保存到 Core Data 中(即 enum
)。
让我们创建一个 struct
然后为其增加协议一致性:
Bookmark struct
|
|
接下来,我们要实现 toCoreData
方法。
协议扩展
当然我们可以为每个 struct
都写新的 toCoreData
方法,但是工作量很大,因为 struct
不支持继承,所以我们不能使用基类的方式。不过我们可以使用 protocol extension
来扩展这个方法到所有相符合的 struct
:
|
|
因为扩展已经被应用到相符合的 struct
,这个方法就可以在 struct
的上下文中被调用。因此,在协议中,self
指的是我们想分析的 struct
。
所以,我们需要做的第一步就是创建一个可以写入我们 Bookmark struct
值的NSManagedObject
。我们该怎么做呢?
一点 Core Data
Core Data
有点啰嗦,所以如果需要创建一个对象,我们需要如下的步骤:
- 获得我们需要创建的实体的名字(字符串)
- 获取
NSManagedObjectContext
,然后为我们的实体创建NSEntityDescription
- 利用这些信息创建
NSManagedObject
。
实现代码如下:
|
|
实现反射
下一步,我们想使用反射 API 来读取 bookmark
对象的属性然后把它写入到 NSManagedObject
实例中。
|
|
我们通过测试 displayStyle
属性的方式来确保这是一个 struct
。
所以现在我们有了一个可以让我们读取属性的 Mirror
,也有了一个可以用来设置属性的 NSManagedObject
。因为 mirror
提供了读取所有 children
的方式,所以我们可以遍历它们并保存它们的值。方式如下:
|
|
太棒了!但是,如果我们试图编译它,它会失败。原因是 setValueForKey
需要一个 AnyObject?
类型的对象,而我们的 children
属性只返回一个 (String?, Any)
类型的 tuple
。也就是说 value
是 Any
类型但是我们需要 AnyObject
类型的。为了解决这个问题,我们要测试 value
的 AnyObject
协议一致性。这也意味着如果得到的属性的类型不符合 AnyObject
协议(比如 enum
),我们就可以抛出一个错误。
|
|
现在,只有在 child
是 AnyObject
类型的时候我们才会调用 setValueForKey
方法。
然后唯一剩下的事情就是返回 NSManagedObject
。完整的代码如下:
|
|
搞定,我们现在已经把 struct
转换为 NSManagedObject
了。
Class 转 Dictionary
|
|
自定义 Mirror
我们之前已经讨论过,创建 Mirror
还有其他的选项。这些选项是非常有用的,比如,你想自己定义 mirror
中对象的哪些部分是可访问的。对于这种情况 Mirror Struct
提供了其他的构造器。
Collection
第一个特殊 init
是为 Collection
量身定做的:
|
|
与之前的 init(reflecting:)
相比,这个构造器允许我们定义更多反射处理的细节。
- 它只对
Collection
有效 - 我们可以设定被反射的对象以及对象的
children
(Collection
的内容)
Class 或者 Struct
第二个可以在 class
或者 struct
上使用。
|
|
有意思的是,这里是由你指定对象的 children
(即属性),指定的方式是通过一个 DictionaryLiteral
,它有点像字典,可以直接用作函数参数。如果我们为 Bookmark struct
实现这个构造器,它看起来是这样的:
|
|
用例
所以留下来让我们思考的问题是什么呢?好的反射用例又是什么呢?很显然,如果你在很多 NSManagedObject
上使用反射,它会大大降低你代码的性能。同时如果只有一个或者两个 struct
,根据自己掌握的struct
领域的知识编写一个序列化的方法会更容易,更高性能且更不容易让人困惑。
而本文展示反射技巧可以当你在有很多复杂的 struct
,且偶尔想对它们中的一部分进行存储时使用。
例子如下:
- 设置收藏夹
- 收藏书签
- 加星
- 记住上一次选择
- 在重新启动时存储 AST 打开的项目
- 在特殊处理时做临时存储
当然除此之外,反射当然还有其他的使用场景:
- 遍历
tuples
- 对类做分析
- 运行时分析对象的一致性
- 自动生成详细日志 / 调试信息(即外部生成对象)
参考链接
Swift 反射 API 及用法:
http://swift.gg/2015/11/23/swift-reflection-api-what-you-can-do/